package eu.darken.octi.sync.module.core

import android.content.Context
import com.gitee.wsl.exception.asLog
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import eu.darken.octi.sync.core.DeviceId
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import okio.ByteString
import timber.log.Timber
import java.io.File
import java.io.IOException
import java.time.Instant


abstract class BaseModuleCache<T : Any> constructor(
    private val context: Context,
    override val moduleId: ModuleId,
    private val tag: String,
    private val dispatcherProvider: CoroutineDispatcher=Dispatchers.IO,
    private val moshi: Moshi,
    private val moduleSerializer: ModuleSerializer<T>,
) : ModuleCache<T> {

    @OptIn(ExperimentalStdlibApi::class)
    private val adapter by lazy { moshi.adapter<CachedModuleData>() }
    private val cacheLock = Mutex()
    private val cacheDir by lazy {
        val moduleCacheBaseDir = File(context.cacheDir, "module_cache")
        File(moduleCacheBaseDir, moduleId.id).also { it.mkdirs() }
    }

    private suspend inline fun <reified T> guard(crossinline block: () -> T) = withContext(dispatcherProvider) {
        cacheLock.withLock { block() }
    }

    private fun DeviceId.getCacheFile(): File = File(cacheDir, this.id)

    @JsonClass(generateAdapter = true)
    data class CachedModuleData(
        @Json(name = "modifiedAt") val modifiedAt: Instant,
        @Json(name = "deviceId") val deviceId: DeviceId,
        @Json(name = "moduleId") val moduleId: ModuleId,
        @Json(name = "data") val data: ByteString,
    )

    private fun ModuleData<T>.toCachedModuleData() = CachedModuleData(
        modifiedAt = modifiedAt,
        deviceId = deviceId,
        moduleId = moduleId,
        data = moduleSerializer.serialize(data)
    )

    private fun CachedModuleData.toModuleData(): ModuleData<T> = ModuleData(
        modifiedAt = modifiedAt,
        deviceId = deviceId,
        moduleId = moduleId,
        data = moduleSerializer.deserialize(data)
    )

    override suspend fun set(deviceId: DeviceId, data: ModuleData<T>?) = guard {
        Timber.d( "set(id=$deviceId, data=$data)" )

        if (data == null) {
            val cacheFile = deviceId.getCacheFile()
            val success = cacheFile.delete()
            if (!success) Timber.w( "Failed to delete $cacheFile")
            return@guard
        }

        try {
            val cacheFile = deviceId.getCacheFile().also {
                if (!it.exists()) it.createNewFile()
            }

            cacheFile.writeText(adapter.toJson(data.toCachedModuleData()))
        } catch (e: Exception) {
            Timber.e( "Failed to cache sync data: ${e.asLog()}")
        }
    }

    override suspend fun get(deviceId: DeviceId): ModuleData<T>? = guard {
        val cacheFile = deviceId.getCacheFile()
        if (!cacheFile.exists()) return@guard null

        val uncachedData = try {
            adapter.fromJson(cacheFile.readText())!!.also {
                Timber.d( "get(id=$deviceId): $it" )
            }
        } catch (e: Exception) {
            Timber.e( "Failed to load cache sync data: ${e.asLog()}" )
            null
        }

        try {
            uncachedData?.toModuleData().also {
                Timber.d( "get(id=$deviceId): $it" )
            }
        } catch (e: Exception) {
            Timber.e( "Failed to deserialize: ${e.asLog()}\nraw=$uncachedData" )
            
            null
        }
    }

    override suspend fun cachedDevices(): Collection<DeviceId> = guard {
        try {
            cacheDir.listFiles()?.map { DeviceId(it.name) } ?: emptyList()
        } catch (e: IOException) {
            Timber.e( "all() failed to read all cached devices." )
            cacheDir.deleteRecursively()
            cacheDir.mkdirs()
            emptyList()
        }
    }
}